import { Matrix4, Object3D } from './three.module.js'

/**
 * Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs
 */

class CSS3DObject extends Object3D {
  constructor (element) {
    super()

    this.element = element || document.createElement('div')
    this.element.style.position = 'absolute'
    this.element.style.pointerEvents = 'auto'
    this.element.style.userSelect = 'none'

    this.element.setAttribute('draggable', false)

    this.addEventListener('removed', function () {
      this.traverse(function (object) {
        if (
          object.element instanceof Element &&
          object.element.parentNode !== null
        ) {
          object.element.parentNode.removeChild(object.element)
        }
      })
    })
  }

  copy (source, recursive) {
    super.copy(source, recursive)

    this.element = source.element.cloneNode(true)

    return this
  }
}

CSS3DObject.prototype.isCSS3DObject = true

class CSS3DSprite extends CSS3DObject {
  constructor (element) {
    super(element)

    this.rotation2D = 0
  }

  copy (source, recursive) {
    super.copy(source, recursive)

    this.rotation2D = source.rotation2D

    return this
  }
}

CSS3DSprite.prototype.isCSS3DSprite = true

//

const _matrix = new Matrix4()
const _matrix2 = new Matrix4()

class CSS3DRenderer {
  constructor () {
    const _this = this

    let _width, _height
    let _widthHalf, _heightHalf

    const cache = {
      camera: { fov: 0, style: '' },
      objects: new WeakMap()
    }

    const domElement = document.createElement('div')
    domElement.style.overflow = 'hidden'

    this.domElement = domElement

    const cameraElement = document.createElement('div')

    cameraElement.style.transformStyle = 'preserve-3d'
    cameraElement.style.pointerEvents = 'none'

    domElement.appendChild(cameraElement)

    this.getSize = function () {
      return {
        width: _width,
        height: _height
      }
    }

    this.render = function (scene, camera) {
      const fov = camera.projectionMatrix.elements[5] * _heightHalf

      if (cache.camera.fov !== fov) {
        domElement.style.perspective = camera.isPerspectiveCamera
          ? fov + 'px'
          : ''
        cache.camera.fov = fov
      }

      if (scene.autoUpdate === true) scene.updateMatrixWorld()
      if (camera.parent === null) camera.updateMatrixWorld()

      let tx, ty

      if (camera.isOrthographicCamera) {
        tx = -(camera.right + camera.left) / 2
        ty = (camera.top + camera.bottom) / 2
      }

      const cameraCSSMatrix = camera.isOrthographicCamera
        ? 'scale(' +
          fov +
          ')' +
          'translate(' +
          epsilon(tx) +
          'px,' +
          epsilon(ty) +
          'px)' +
          getCameraCSSMatrix(camera.matrixWorldInverse)
        : 'translateZ(' +
          fov +
          'px)' +
          getCameraCSSMatrix(camera.matrixWorldInverse)

      const style =
        cameraCSSMatrix +
        'translate(' +
        _widthHalf +
        'px,' +
        _heightHalf +
        'px)'

      if (cache.camera.style !== style) {
        cameraElement.style.transform = style

        cache.camera.style = style
      }

      renderObject(scene, scene, camera, cameraCSSMatrix)
    }

    this.setSize = function (width, height) {
      _width = width
      _height = height
      _widthHalf = _width / 2
      _heightHalf = _height / 2

      domElement.style.width = width + 'px'
      domElement.style.height = height + 'px'

      cameraElement.style.width = width + 'px'
      cameraElement.style.height = height + 'px'
    }

    function epsilon (value) {
      return Math.abs(value) < 1e-10 ? 0 : value
    }

    function getCameraCSSMatrix (matrix) {
      const elements = matrix.elements

      return (
        'matrix3d(' +
        epsilon(elements[0]) +
        ',' +
        epsilon(-elements[1]) +
        ',' +
        epsilon(elements[2]) +
        ',' +
        epsilon(elements[3]) +
        ',' +
        epsilon(elements[4]) +
        ',' +
        epsilon(-elements[5]) +
        ',' +
        epsilon(elements[6]) +
        ',' +
        epsilon(elements[7]) +
        ',' +
        epsilon(elements[8]) +
        ',' +
        epsilon(-elements[9]) +
        ',' +
        epsilon(elements[10]) +
        ',' +
        epsilon(elements[11]) +
        ',' +
        epsilon(elements[12]) +
        ',' +
        epsilon(-elements[13]) +
        ',' +
        epsilon(elements[14]) +
        ',' +
        epsilon(elements[15]) +
        ')'
      )
    }

    function getObjectCSSMatrix (matrix) {
      const elements = matrix.elements
      const matrix3d =
        'matrix3d(' +
        epsilon(elements[0]) +
        ',' +
        epsilon(elements[1]) +
        ',' +
        epsilon(elements[2]) +
        ',' +
        epsilon(elements[3]) +
        ',' +
        epsilon(-elements[4]) +
        ',' +
        epsilon(-elements[5]) +
        ',' +
        epsilon(-elements[6]) +
        ',' +
        epsilon(-elements[7]) +
        ',' +
        epsilon(elements[8]) +
        ',' +
        epsilon(elements[9]) +
        ',' +
        epsilon(elements[10]) +
        ',' +
        epsilon(elements[11]) +
        ',' +
        epsilon(elements[12]) +
        ',' +
        epsilon(elements[13]) +
        ',' +
        epsilon(elements[14]) +
        ',' +
        epsilon(elements[15]) +
        ')'

      return 'translate(-50%,-50%)' + matrix3d
    }

    function renderObject (object, scene, camera, cameraCSSMatrix) {
      if (object.isCSS3DObject) {
        object.onBeforeRender(_this, scene, camera)

        let style

        if (object.isCSS3DSprite) {
          // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/

          _matrix.copy(camera.matrixWorldInverse)
          _matrix.transpose()

          if (object.rotation2D !== 0) { _matrix.multiply(_matrix2.makeRotationZ(object.rotation2D)) }

          _matrix.copyPosition(object.matrixWorld)
          _matrix.scale(object.scale)

          _matrix.elements[3] = 0
          _matrix.elements[7] = 0
          _matrix.elements[11] = 0
          _matrix.elements[15] = 1

          style = getObjectCSSMatrix(_matrix)
        } else {
          style = getObjectCSSMatrix(object.matrixWorld)
        }

        const element = object.element
        const cachedObject = cache.objects.get(object)

        if (cachedObject === undefined || cachedObject.style !== style) {
          element.style.transform = style

          const objectData = { style: style }
          cache.objects.set(object, objectData)
        }

        element.style.display = object.visible ? '' : 'none'

        if (element.parentNode !== cameraElement) {
          cameraElement.appendChild(element)
        }

        object.onAfterRender(_this, scene, camera)
      }

      for (let i = 0, l = object.children.length; i < l; i++) {
        renderObject(object.children[i], scene, camera, cameraCSSMatrix)
      }
    }
  }
}

export { CSS3DObject, CSS3DSprite, CSS3DRenderer }
